package classifiers;
/*
 *    This program is free software; you can redistribute it and/or modify
 *    it under the terms of the GNU General Public License as published by
 *    the Free Software Foundation; either version 2 of the License, or
 *    (at your option) any later version.
 *
 *    This program is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *    GNU General Public License for more details.
 *
 *    You should have received a copy of the GNU General Public License
 *    along with this program; if not, write to the Free Software
 *    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */

/*
 *    BinaryRelevance.java
 *    Copyright (C) 2009-2012 Aristotle University of Thessaloniki, Greece
 */

import java.util.Arrays;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletionService;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorCompletionService;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

import classifiers.helper.ClassificationMethod;
import classifiers.helper.ConcurrentBinaryRelevanceTransformation;
import classifiers.helper.PredictionForLabel;
import mulan.classifier.MultiLabelOutput;
import mulan.classifier.transformation.TransformationBasedMultiLabelLearner;
import mulan.data.MultiLabelInstances;
import mulan.transformations.BinaryRelevanceTransformation;
import weka.classifiers.AbstractClassifier;
import weka.classifiers.Classifier;
import weka.classifiers.trees.J48;
import weka.core.Instance;
import weka.core.Instances;

/**
 *
 * @author Andrej Gajduk
 * @version 2013.08.15
 */
public class ConcurrentBinaryRelevance extends TransformationBasedMultiLabelLearner {

    /**
	 * 
	 */
	private static final long serialVersionUID = 5124889342337134398L;
	
	/**
     * The ensemble of binary relevance models. These are Weka Classifier
     * objects.
     */
    Classifier[] ensemble;
    /**
     * The correspondence between ensemble models and labels
     */
    private String[] correspondence;
    
    private double threshold;
    private int desired_num_threads;
    private int queue_min_size;
    
    public void setThreshold(double threshold) {
		this.threshold = threshold;
	}

//	private ConcurrentBinaryRelevanceTransformation cbrt;
    private BinaryRelevanceTransformation cbrt;

   private ConcurrentBinaryRelevance(
			ConcurrentBinaryRelevanceBuilder builder) {
	    super(builder.classifier);
	    this.threshold = builder.threshold;
	    this.desired_num_threads = builder.desired_num_threads;
	}

	public static class ConcurrentBinaryRelevanceBuilder {
    	private Classifier classifier;
    	private int desired_num_threads = Runtime.getRuntime().availableProcessors()+1;
    	private double threshold = .5;
    	
    	public ConcurrentBinaryRelevanceBuilder(Classifier classifier) {
    		this.classifier = classifier;
    	}
    	
    	public ConcurrentBinaryRelevanceBuilder withDesiredNumberThreads(int desired_num_threads) {
    		this.desired_num_threads = desired_num_threads;
    		return this;
    	}
    	
    	public ConcurrentBinaryRelevanceBuilder withThreshold(double threshold) {
    		this.threshold = threshold;
    		return this;
    	}
    	
    	public ConcurrentBinaryRelevance build() {
    		return new ConcurrentBinaryRelevance(this);
    	}
    	
    }
	
	
	
	void prepareToBuild(MultiLabelInstances train) {
		numLabels = train.getNumLabels();
		labelIndices = train.getLabelIndices();
		this.queue_min_size = Math.max(desired_num_threads,numLabels);
        ensemble = new Classifier[numLabels];
        correspondence = new String[numLabels];
        for (int i = 0; i < numLabels; i++) {
            correspondence[i] = train.getDataSet().attribute(labelIndices[i]).name();
        }
        debug("preparing shell");
//        cbrt = new ConcurrentBinaryRelevanceTransformation(train);
      cbrt = new BinaryRelevanceTransformation(train);
	}


	protected void buildInternal(MultiLabelInstances train) throws Exception {
		prepareToBuild(train);
        ThreadPoolExecutor executor = new ThreadPoolExecutor(desired_num_threads, queue_min_size, Long.MAX_VALUE,TimeUnit.MINUTES, new ArrayBlockingQueue<Runnable>(queue_min_size));
        for (int i = 0; i < numLabels; i++) {
            ensemble[i] = AbstractClassifier.makeCopy(baseClassifier);
        }
        int label_indices[] = train.getLabelIndices();
        for (int i = 0; i < numLabels; i++) {
        	final Classifier base_classifier = ensemble[i];
        	final int label_idx = label_indices[i];
        	final int idx = i;
//        	final ConcurrentBinaryRelevanceTransformation cbrt_final =  cbrt;
           final BinaryRelevanceTransformation cbrt_final = cbrt;
        	executor.execute(new Runnable() {

				@Override
				public void run() {
					Instances transformed_instances;
					try {
//						transformed_instances = cbrt_final.transformInstances(label_idx);
						transformed_instances = cbrt_final.transformInstances(idx);
						base_classifier.buildClassifier(transformed_instances);
					} catch (Exception e) {
						e.printStackTrace();
					}
				}
				
			});
        }
        executor.shutdown();
        executor.awaitTermination(Long.MAX_VALUE, TimeUnit.MINUTES);
        
    }
	
    protected MultiLabelOutput makePredictionInternal(final Instance instance) {
        boolean[] bipartition = new boolean[numLabels];
        double[] confidences = new double[numLabels];
        ThreadPoolExecutor executor = new ThreadPoolExecutor(numLabels,numLabels,Long.MAX_VALUE,TimeUnit.MINUTES,new ArrayBlockingQueue<Runnable>(numLabels)); 
        CompletionService<PredictionForLabel> completitionService = 
        		new ExecutorCompletionService<PredictionForLabel>(executor);
        for (int counter = 0; counter < numLabels; counter++) {
        	final int label_idx = labelIndices[counter];
        	final int idx = counter;
        	final Classifier base_classifier = ensemble[counter]; 
//        	final ConcurrentBinaryRelevanceTransformation cbrt_final = cbrt;
        	final BinaryRelevanceTransformation cbrt_final = cbrt;
        	completitionService.submit(new Callable<PredictionForLabel>() {
				
				@Override
				public PredictionForLabel call() throws Exception {
//					Instance transformed_instance = cbrt_final.transformInstance(instance, label_idx,false);
					Instance transformed_instance = cbrt_final.transformInstance(instance, idx);
					return new PredictionForLabel(label_idx,base_classifier.distributionForInstance(transformed_instance)[1]);
				}
				
			});
        }
        try {
	        for (int counter = 0; counter < numLabels; counter++) {
	        	Future<PredictionForLabel> f = completitionService.take();
	        	PredictionForLabel pf = f.get();
	        	int idx = pf.getLabel_idx()-instance.numAttributes()+numLabels;
				confidences[idx] = pf.getConfidence();
				bipartition[idx] = confidences[counter]>threshold;
	        }
        } catch (Exception e) {
			e.printStackTrace();
		}
        try {
        executor.shutdown();
			executor.awaitTermination(Long.MAX_VALUE, TimeUnit.MINUTES);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
        System.out.println(threshold);
        System.out.println("Attributes");
        System.out.print("[");
        for ( int i = 0 ; i < numLabels ; ++i ) {
        	System.out.print(instance.attribute(instance.numAttributes()-numLabels+i).name()+",");
        }
        System.out.println();
        System.out.println("Confidences");
        System.out.println(Arrays.toString(confidences));
        System.out.println("Guessed");
        System.out.println(Arrays.toString(bipartition));
        System.out.println("True");
        System.out.print("[");
        for ( int i = 0 ; i < numLabels ; ++i ) {
        	System.out.print((instance.value(instance.numAttributes()-numLabels+i)>.1)+",");
        }
        System.out.println();
        MultiLabelOutput mlo = new MultiLabelOutput(bipartition, confidences);
        return mlo;
    }

    /**
     * Returns the model which corresponds to the label with labelName
     *
     * @param labelName
     * @return the corresponding model or null if the labelIndex is wrong
     */
    public Classifier getModel(String labelName) {
        for (int i = 0; i < numLabels; i++) {
            if (correspondence[i].equals(labelName)) {
                return ensemble[i];
            }
        }
        return null;
    }
    
    @Override
    public String toString() {
		return ConcurrentBinaryRelevance.getNameForCalssifier(baseClassifier);
	}
    
    public static String getNameForCalssifier(Classifier classifier) {
    	if ( classifier instanceof LibSVM ) {
			LibSVM svm = (LibSVM) classifier;
			return ClassificationMethod.svm+"_"+svm.getCost()+"_"+svm.getGamma();
		}
		if ( classifier instanceof J48 ) {
			return  ClassificationMethod.dt.toString();
		}
		return "unknown";
    }

}